package map.core;

import com.google.common.collect.ImmutableList;
import map.core.util.MapHelper;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.geometry.GeneralBounds;
import org.geotools.util.factory.Hints;
import ucar.ma2.Array;
import ucar.ma2.Index;
import ucar.ma2.InvalidRangeException;
import ucar.ma2.Range;
import ucar.nc2.Dimension;
import ucar.nc2.NetcdfFile;
import ucar.nc2.Variable;

import java.awt.image.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class NetCDFLayer extends RasterLayer {
    private final Array latValues;
    private final Array lonValues;
    Variable variable;
    Variable lonVariable;
    Variable latVariable;
    Variable zVariable;
    Variable sliceVariable;
    NetcdfFile netcdfFile;

    int lonVariableOrder =-1;
    int latVariableOrder = -1;
    int sliceVariableOrder =-1;
    int zVariableOrder = -1;

    float minStep ;
    private final DoubleRange lonRange;
    private final DoubleRange latRange;


    public NetCDFLayer(Variable variable, Variable lonVariable, Variable latVariable,Variable zVariable, Variable sliceVariable, NetcdfFile ncfile,Map<String,String> map) {
        this.variable = variable;
        this.lonVariable = lonVariable;
        this.latVariable = latVariable;
        this.zVariable = zVariable;
        this.sliceVariable = sliceVariable;
        this.netcdfFile = ncfile;

        List<Dimension> dimensionList = variable.getDimensions();

        if(lonVariable !=null){
            lonVariableOrder = getDimensionOrder(dimensionList,lonVariable.getFullName(),map);
        }

        if(latVariable !=null){
            latVariableOrder= getDimensionOrder(dimensionList,latVariable.getFullName(),map);
        }

        if(sliceVariable !=null){
            sliceVariableOrder= getDimensionOrder(dimensionList,sliceVariable.getFullName(),map);
        }

        if(zVariable  !=null){
            zVariableOrder = getDimensionOrder(dimensionList,zVariable.getFullName(),map);
        }

        this.minStep = this.getMinStep();

        try {
            this.latValues=  this.latVariable.read();
            this.lonValues = this.lonVariable.read();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        this.lonRange=  this.calLonRange();

        this.latRange = this.calLatRange();

        //System.out.println(this.minStep);
    }

    private float[] getSteps( Array data){
        int size = (int) data.getSize();
        float[] steps = new float[size-1];
        for(int i = 0; i < size-1  ;i  ++){
            steps[i] = data.getFloat(i+1) - data.getFloat(i);
        }
        return steps;
    }
    private float getMinStep() {
        try {

            Array latData = latVariable.read();
            float[] latSteps = getSteps(latData);


            Array lonData = lonVariable.read();
            float[] lonSteps = getSteps(lonData);


            Arrays.sort(latSteps);
            Arrays.sort(lonSteps);

            return Math.min(latSteps[0],lonSteps[0]);


        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    private int getDimensionOrder(List<Dimension> dimensionList,String name,Map<String,String> map){
        for(int i = 0 ;i  < dimensionList.size() ; i ++){

            String key = "";
            for (Map.Entry<String, String> entry : map.entrySet()) {
                if (entry.getValue().equals(name)) {
                    key = entry.getKey();
                }
            }


            if(dimensionList.get(i).getName().equals(key)){
                return i;
            }
        }
        throw new RuntimeException("无效name："+name);
    }

    public NetCDFLayerBaseInfo  getBaseInfo(){
        if(sliceVariable == null && lonVariable != null && latVariable != null){
            String name = variable.getFullName();
            return new NetCDFLayerBaseInfo(name,this.getBounds(),null);
        }
        else if(sliceVariable != null && lonVariable != null && latVariable != null ){
            try {
                String name = variable.getFullName();
                Array sliceData = this.sliceVariable.read();
                return new NetCDFLayerBaseInfo(name, this.getBounds (), createDimension(this.sliceVariable));

            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        else {
            return null;
        }
    }


    private NetCDFDimension createDimension(Variable variable){

        try {
            String name = variable.getFullName();
            String unit = variable.getUnitsString();
            String description = variable.getDescription();
            Object values = variable.read().copyTo1DJavaArray();
            return new NetCDFDimension(name,unit,description,values);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }



    private DoubleRange calLonRange(){
        return getRange(this.lonValues,true);
    }

    public int getWidth(){
        return (int) (this.getLonRange().getWidth()/this.minStep);
    }

    public int getHeight(){
        return (int) (this.getLatRnage().getWidth()/this.minStep);
    }

    private DoubleRange calLatRange(){
        return getRange(this.latValues,true);
    }


    private Array getRawData(){
        try {
            Array data = null;
            if(sliceVariable != null){

            }
            else {
                data= variable.read();
            }
            return data;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private DoubleRange getSliceRange(){
        try {
            return getRange(sliceVariable.read(),false);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    public LonLatBounds getBounds(){
        DoubleRange lonRange = this.calLonRange();
        DoubleRange  latRange = this.calLatRange();
        return new LonLatBounds(lonRange.getMin(),lonRange.getMax(),latRange.getMin(),latRange.getMax());
    }

    public ImmutableList<Dimension> getz(){
        if(zVariable != null){
            return zVariable.getDimensions();
        }else{
            return null;
        }
    }

    /**
     * 获取属性最大值最小值
     * @param name 属性
     *  @param  notCounting 不计入的数值，去掉这个数值在计算最大值最小值
     * @return
     */
    public DoubleRange getLayerValueRange(String name,double notCounting){

        Variable variable = findRequireVariable(this.netcdfFile,name);
        try {
            Array array  = variable.read();
            double min = Double.MAX_VALUE;
            double max = Double.MIN_VALUE;

            for(int i = 0 ;i < array.getSize() ; i ++){
                double value = array.getDouble(i);
                if(value != notCounting){
                    if(value < min){
                        min = value;
                    }
                    if(value > max){
                        max = value;
                    }
                }

            }

            return new DoubleRange(min,max);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    private static Variable findRequireVariable(NetcdfFile ncfile, String variableName){
        Variable variable = ncfile.findVariable(variableName);
        if(variable!= null){
            return variable;
        }
        else {
            throw new RuntimeException("无效variableName " + variableName);
        }
    }


    private DoubleRange getRange(Array data, boolean useGridMode){




        long size = data.getSize();
        double  min = data.getDouble(0);
        double max = data.getDouble((int) (size-1));

        if(useGridMode){
            // double gridSpan = data.getDouble(1) - data.getDouble(0);
            // min -= gridSpan/2;

            // max += gridSpan/2;
        }

        if(min >= max){
            System.out.println(variable);
            System.out.println(data);
            throw new RuntimeException("无效Range");
        }

        return  new DoubleRange(min,max);

    }



    private Array getRawData(int slice){
        try {
            Array rawData = null;
            int totalDimSize=  this.variable.getDimensions().size();

            List<Range> ranges = new ArrayList<>();
            for(int i = 0 ;i < totalDimSize ; i ++){
                if(sliceVariableOrder ==i){
                    ranges.add(new Range(slice,slice));
                }
                else if(zVariableOrder == i){
                    ranges.add(new Range(0,0));
                }
                else {
                    ranges.add(new Range(0,variable.getShape(i)-1));
                }
            }
            rawData = variable.read(ranges);
            rawData = rawData.reduce();

            if(lonVariableOrder > latVariableOrder){
                rawData= rawData.transpose(0,1);
            }
            return  rawData;
        } catch (IOException | InvalidRangeException e) {
            throw new RuntimeException(e);
        }
    }




    private int findNearestIndex(Array data,float target){
        int size = (int) data.getSize();
        int left = 0;
        int right =size - 1;
        int closestIndex = left;

        while (left <= right) {
            int mid = (left + right) / 2;

            // 如果当前值是目标值，直接返回
            if (data.getFloat(mid) == target) {
                return mid;
            }

            // 更新最接近的下标
            if (Math.abs(data.getFloat(mid) - target) < Math.abs(data.getFloat(closestIndex) - target)) {
                closestIndex = mid;
            }

            // 调整搜索区间
            if (data.getFloat(mid) < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return closestIndex;
    }


    private void calRawDataIndex(Index index, float lon, float lat){
        int i  = findNearestIndex(this.lonValues,lon);
        int j = findNearestIndex(this.latValues,lat);
        index.set(i,j);
    }

    private float getSampleValue(Array rawData,Index index,float lon,float lat){
        calRawDataIndex(index,lon,lat);
        float value = rawData.getFloat(index);
        return value;
    }

    private   GridCoverage2D createGridCoverage2D(DoubleRange lonRange, DoubleRange latRange, int width, int height, int slice, float gridSize) throws IOException {
        int bands = 1;
        int[] bandOffsets = {0};


        SampleModel sampleModel = new PixelInterleavedSampleModel(DataBuffer.TYPE_FLOAT, width, height, bands, width  * bands, bandOffsets);
        DataBuffer buffer = new DataBufferFloat(width * height * bands);
        WritableRaster raster = Raster.createWritableRaster(sampleModel, buffer, null);
        Array rawData  = getRawData(slice);

        float minLon = (float) lonRange.getMin();
        float mimLat = (float) latRange.getMin();

        float lon;
        float lat;
        Index index = rawData.getIndex();


        for(int i = 0 ;i < width ; i ++){
            for(int j = 0 ; j < height ; j ++){

                lon  = i * gridSize + minLon;
                lat = j * gridSize + mimLat;
                calRawDataIndex(index,lon,lat);
                float value = getSampleValue(rawData,index,lon,lat);
                //index.set(i,j);
                raster.setSample(i,height-j-1,0,value);
            }
        }

        final Hints hints = new Hints();
        final GridCoverageFactory factory = CoverageFactoryFinder.getGridCoverageFactory(hints);


        final GeneralBounds envelope = new GeneralBounds(MapHelper.CRS_4326);
        envelope.setRange(0, lonRange.getMin(), lonRange.getMax());
        envelope.setRange(1,latRange.getMin(),latRange.getMax());

        return factory.create("cov",raster,envelope);
    }


    /**
     * @param slice 从0开始
     * @return
     */
    public GridCoverage2D getGridData(int slice){
        try {
            DoubleRange lonRange = this.getLonRange();
            DoubleRange latRange = this.getLatRnage();
            int width = this.getWidth();
            int height = this.getHeight();
            return createGridCoverage2D(lonRange,latRange,width,height,slice,minStep);
        } catch (IOException e) {
            throw  new RuntimeException(e);
        }
    }

    public   GridCoverage2D getGridData(){
        return this.getGridData(0);
    }

    public DoubleRange getLonRange() {
        return lonRange;
    }

    public DoubleRange getLatRnage() {
        return latRange;
    }
}
